package fons.navigator.views.piemap;

import java.awt.Color;
import java.awt.Font;
import java.awt.Shape;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import edu.umd.cs.piccolo.PCamera;
import edu.umd.cs.piccolo.PNode;
import edu.umd.cs.piccolo.nodes.PText;
import edu.umd.cs.piccolo.util.PBounds;
import fons.model.ontology.Context;
import fons.model.ontology.Dimension;
import fons.model.ontology.SelectionDefinition;
import fons.model.ontology.SelectionUpdateListener;
import fons.model.ontology.Value;
import fons.model.ontology.ValueSpace;
import fons.navigator.util.GeomUtils;
import fons.navigator.util.box2d.DynamicPlacer;
import fons.navigator.util.map.MapProjection;
import fons.navigator.util.piccolo.Lasso;
import fons.navigator.util.piccolo.Legend;
import fons.navigator.views.NavigatorAbstractView;
import fons.navigator.views.VisualCanvas;
import fons.util.Disposable;

public class PieMap extends PNode implements SelectionUpdateListener, Disposable {
	private static final long serialVersionUID = 1L;
	private final Font CAPTION_FONT = new Font("Arial", Font.BOLD, 16);

	private final NavigatorAbstractView view;

	private final Lasso lasso;
	private final Legend legend;

	private List<Target> targets;
	private PBounds targetBounds;

	private boolean showSelectedOnly = true;
	private final DynamicPlacer dynamicPlacer = new DynamicPlacer();

	private Dimension currentValueDimension;
	private PText currentValueDimensionLabel;

	private final MapProjection mapProjection;
	private PGeoMap areaMap;
	private PNode mapBackgroundImage;  

	private final Context context;
	private final Dimension pieDimension;
	private final Set<Dimension> targetDimensions = Sets.newHashSet();
	private final Set<Dimension> valueDimensions = Sets.newTreeSet(Dimension.COMPARE_BY_DEFINITION);
	private final Set<Dimension> partsParent = Sets.newTreeSet(Dimension.COMPARE_BY_DEFINITION);
	private final Set<Dimension> subParts = Sets.newTreeSet(Dimension.COMPARE_BY_DEFINITION);	
	
	private final ValueSpace baseSpace;
	private ValueSpace currentValueSpace;
	private Map<Dimension, ValueSpace> currentTargetSpaces;

	public PieMap(final NavigatorAbstractView view, 
			final Context context,
			final Set<Dimension> targets,
			final Set<Dimension> values,
			final Dimension partsParent,
			final Set<Dimension> subParts,
			final Set<Dimension> filters,
			boolean showLegend,
			MapProjection proj) {

		this.view = view;
		this.context = context;
		this.pieDimension = partsParent;
		this.targetDimensions.addAll(targets);
		this.valueDimensions.addAll(values);
		this.partsParent.addAll(partsParent.children());
		this.subParts.addAll(subParts);
		this.baseSpace = ValueSpace.of(filters);

		this.mapProjection = proj;
		this.currentValueDimension = Iterables.getLast(valueDimensions);
		this.currentValueSpace = baseSpace.subSpace(currentValueDimension);
		this.currentTargetSpaces = this.currentValueSpace.split(targetDimensions);

		legend = new Legend(new Color(220, 220, 220, 200), new PieChartLegendItemProducer(this));
		setLegendVisible(showLegend);
		legend.update();

		getCamera().addPropertyChangeListener(new PropertyChangeListener() {
			public void propertyChange(PropertyChangeEvent evt) {
				if (evt.getPropertyName() == PCamera.PROPERTY_BOUNDS) {
					fitInCameraView();
					updateCurrentValueDimensionLabel();

				} else if (evt.getPropertyName() == PCamera.PROPERTY_VIEW_TRANSFORM) {
					updateTargetPositionsInCamera();
				}
			}
		});

		lasso = new Lasso(getVisualCanvas().getCamera(), Color.red) {
			@Override
			public void selectionMade(Shape shape) {
				for (Target vn: getTargets()) {
					if (shape.contains(vn.getPoint())) {
						context.getStandardSelection().select("TARGETS", vn.getTargetDimension());
						vn.moveToFront();
					} else {
						context.getStandardSelection().unselect("TARGETS", vn.getTargetDimension());
						vn.moveToBack();
					}
				}
				updateDynamicPositions();
				legend.update();
			}
		};

		context.getStandardSelection().addSelectionUpdateListener(this);
		getVisualCanvas().getCamera().addInputEventListener(lasso);
	}

	public Dimension getPieDimension() {
		return pieDimension;
	}
	
	public NavigatorAbstractView getAbstractView() {
		return this.view;
	}

	public Set<Dimension> getParts() {
		return Collections.unmodifiableSet(partsParent);
	}

	private final Map<Dimension, Value> minValues = Maps.newHashMap();
	private final Map<Dimension, Value> maxValues = Maps.newHashMap();
	
	public Set<Dimension> getSubParts() {
		return Collections.unmodifiableSet(subParts);
	}

	public Value getMin (Dimension type, Set<Dimension> filter) {		
		if (minValues.containsKey(type)) return minValues.get(type);

		final Set<Dimension> filterSpaceDimensions = Sets.newHashSet();
		filterSpaceDimensions.add(type);
		filterSpaceDimensions.addAll(baseSpace.getDimensions());

		final ValueSpace filterSpace = ValueSpace.of(filterSpaceDimensions);		
		final Value min = filterSpace.getMin(filter);
		if (min != Context.missingValue) {
			minValues.put(type, min);
		}
		return min;
	}

	public Value getMax (Dimension type, Set<Dimension> filter) {
		if (maxValues.containsKey(type)) return maxValues.get(type);

		final Set<Dimension> filterSpaceDimensions = Sets.newHashSet();
		filterSpaceDimensions.add(type);
		filterSpaceDimensions.addAll(baseSpace.getDimensions());

		final ValueSpace filterSpace = ValueSpace.of(filterSpaceDimensions);
		final Value max = filterSpace.getMax(filter);
		if (max != Context.missingValue) {
			maxValues.put(type, max);
		}
		return max;
	}

	public VisualCanvas getVisualCanvas() {
		return view.getVisualCanvas();
	}

	public void setCurrentValueDimension(final Dimension dimension) {
		currentValueDimension = dimension;
		currentValueSpace = baseSpace.subSpace(currentValueDimension);
		currentTargetSpaces = currentValueSpace.split(targetDimensions);

		for (Target target: targets) {
			target.setTargetSpace(currentTargetSpaces.get(target.getTargetDimension()));
		}
		updateCurrentValueDimensionLabel();
	}

	public void updateCurrentValueDimensionLabel() {
		if (currentValueDimensionLabel != null) {
			getCamera().removeChild(currentValueDimensionLabel);
		}
		currentValueDimensionLabel = new PText();
		final PText label = currentValueDimensionLabel;
		label.setTextPaint(Color.BLACK);
		label.setFont(CAPTION_FONT);
		label.setVisible(true);
		
		getCamera().addChild(label);

		if (label.getVisible()) {
			PBounds cb = getCamera().getBoundsReference();
			label.setText(currentValueDimension.getName());

			float height = (float)(cb.height * 0.08);
			float fontSize = label.getFont().getSize2D();
			if (fontSize != height) {
				label.setFont(label.getFont().deriveFont(height));
			}

			label.setBounds(
					cb.getMaxX() - label.getWidth() - 10,
					cb.y + (cb.height - height - 10), label.getWidth(), height);
		}
	}

	public Dimension getCurrentValueDimension() {
		return currentValueDimension;
	}

	public PGeoMap getAreaMap() {
		return areaMap;
	}

	@Override
	public String getName() {
		return "Landkarte";
	}

	public void setLegendVisible(boolean visible) {
		legend.setVisible(visible);
	}

	public Legend getLegend() {
		return legend;
	}

	public void addTargetsToCamera() {
		getCamera().addChild(legend);
		createVisuals();
	}

	public void removeTargetsFromCamera() {
		getCamera().removeChild(legend);
		getCamera().removeChild(currentValueDimensionLabel);
		getCamera().removeChildren(targets);
		getCamera().removeInputEventListener(lasso); 
	}

	public MapProjection getMapProjection() {
		return mapProjection;
	}

	private void createVisuals() {
		if (targets != null) {
			for (Target vn : targets) {
				getCamera().removeChild(vn);
			}
		}

		targets = Lists.newArrayList();

		for (Dimension target: targetDimensions) {
			if (target.canGet("Latitude", Double.class) && target.canGet("Longitude", Double.class)) {
				final double lat = target.get("Latitude", Double.class);
				final double lon = target.get("Longitude", Double.class);

				Point2D p = getMapProjection().project(lon, lat);
				Target vtarget = new Target(this, target, currentTargetSpaces.get(target), p.getX(), p.getY());

				getCamera().addChild(vtarget);
				targets.add(vtarget);
			}
		}

		updateTargetPositionsInCamera();
		updateCurrentValueDimensionLabel();
	}


	public void setAreaMap(PGeoMap areaMap) {
		if (this.areaMap != null) {
			removeChild(this.areaMap);
		}

		this.areaMap = areaMap;

		if (areaMap != null) {
			addChild(areaMap);
			areaMap.moveToBack();
			if (mapBackgroundImage != null) {
				mapBackgroundImage.moveToBack();
			}
		}
	}

	public List<Target> getTargets() {
		return Collections.unmodifiableList(targets);
	}

	private PBounds getTargetsBounds() {
		if (targetBounds == null) {
			PBounds b = null;
			for (Target target : targets) {
				if (b == null) {
					b = target.getBounds();
				} else {
					Rectangle2D.union(b, target.getBoundsReference(), b);
				}
			}
			targetBounds = b;
		}
		return targetBounds;
	}

	public void fitInCameraView() {
		PBounds boundRect = getTargetsBounds();
		boundRect = (PBounds)getCamera().globalToLocal(boundRect);
		getCamera().animateViewToCenterBounds(GeomUtils.growRectByRelativeSize(boundRect, .05, .05, .05, .05), true, 0);
	}
	
	public void updateDynamicPositions() {
		dynamicPlacer.calculatePositions();
		dynamicPlacer.updatePositions();
	}

	private void updateTargetPositionsInCamera() {
		for (Target vn : targets) {
			vn.updatePositionInCamera(getCamera());
		}
	}

	public PCamera getCamera() {
		return view.getVisualCanvas().getCamera();
	}

	public DynamicPlacer getDynamicPlacer() {
		return this.dynamicPlacer;
	}

	public void showSelectedTargetsOnly(boolean b) {
		if (b != showSelectedOnly) {
			showSelectedOnly = b;
			for (Target target: getTargets()) {
				if (showSelectedOnly) {
					target.newDrawingStrategy(target.isSelected() ? PieChartDrawingStrategy.class : CircleDrawingStrategy.class);
				} else {
					target.newDrawingStrategy(PieChartDrawingStrategy.class);
				}
			}
			updateDynamicPositions();
		}
	}

	public boolean showSelectedOnly() {
		return showSelectedOnly;
	}

	@Override
	public void beforeSelectionChanged() {
	}

	@Override
	public void afterSelectionChanged() {
		this.updateDynamicPositions();
		this.legend.update();
	}

	@Override
	public void dispose() {
		context.getStandardSelection().removeSelectionUpdateListener(this);
		for (Target target: this.targets) {
			target.dispose();
		}
	}

	@Override
	public void selectedSelection(SelectionDefinition definition) {		
	}

	public PNode getMapBackgroundImage() {
		return this.mapBackgroundImage;
	}

	public void setShowLegends(boolean showLegends) {
		this.showLegends = showLegends;
		for (Target t: targets) {
			t.updateLegend();
		}
		this.updateDynamicPositions();
	}

	public boolean getShowLegends() {
		return this.showLegends;
	}

	private boolean showLegends = false;
	private double pieDistance = 1.5; //in radii

	public void increasePieDistances() {
		pieDistance *= 1.05;
		this.updateDynamicPositions();
	}

	public void decreasePieDistances() {
		pieDistance *= 0.95;
		this.updateDynamicPositions();
	}

	public double getPieDistance() {
		return this.pieDistance;
	}

	private int legendItems = 6;

	public void increaseLegendItems() {
		this.legendItems ++;
		this.redraw();
		this.updateDynamicPositions();
	}

	public void decreaseLegendItems() {
		if (--this.legendItems < 1) this.legendItems = 1;
		this.redraw();
		this.updateDynamicPositions();
	}

	public int getLegendItemsCount() {
		return this.legendItems;
	}

	private static double pieSize = 1;

	public void increasePieSize() {
		pieSize *= 1.05;
		this.redraw();
		this.updateDynamicPositions();
		this.legend.update();
	}

	public void decreasePieSize() {
		pieSize *= 0.95;
		this.redraw();
		this.updateDynamicPositions();
		this.legend.update();
	}

	public static double getPieSize() {
		return pieSize;
	}

	public List<Dimension> getValueDimensions() {
		return Lists.newArrayList(this.valueDimensions);
	}

	public ValueSpace getCurrentValueSpace() {
		return currentValueSpace;
	}

	public Set<Dimension> getTargetDimensions() {
		return targetDimensions;
	}

	public Context getContext() {
		return this.context;
	}

	public void redraw() {
		for (Target t: targets) {
			Class<? extends DrawingStrategy> drawingStrategy = t.getDrawingStrategy().getClass();
			if (drawingStrategy.equals(NullDrawingStrategy.class)) {
				drawingStrategy = this.showSelectedOnly() ? 
						(t.isSelected() ? PieChartDrawingStrategy.class : CircleDrawingStrategy.class)
						: PieChartDrawingStrategy.class;
			}
			t.newDrawingStrategy(t.getDrawingStrategy().getClass());
		}
	}
}
